﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
using fastJSON;
using gtasks.Auth;
using gtasks.Model;
using gtasks.Utils;
using pub.Functions;
using pub.NET20Fix;
using todo;
using todo.Model;
using todo.Persistence;

namespace gtasks
{
    internal class Synchronizer
    {
        private string _filePath;
        private string taskListId;
        private bool _changed;
        private string _token;
        private bool _aborting;
        private Dictionary<int, string> _newIdMap;

        public event Action<Exception> OnException;

        public void FireOnException(Exception e)
        {
            var handler = OnException;
            if (handler != null) handler(e);
        }

        public Dictionary<int, string> NewIdMap
        {
            get { return _newIdMap; }
        }

        //todo: what to do when out of sync?
        public void Sync(string filePath, TodoList todoList)
        {
            _filePath = filePath;
            taskListId = todoList.Id;
//            Thread.Sleep(3000);
            Debug.Print("@@@@ synchronizing... @" + DateTime.Now);
            AuthManager.Instance.RequestToken(token => {
                _token = token;
                try
                {
                    var tasks = TaskManager.Instance.LoadTasks(taskListId, token);
                    Sync(todoList, tasks);
                }
                catch (Exception e)
                {
                    Dispatcher.Current.Invoke(() => FireOnException(e));
                }
            });
        }

        private void Sync(TodoList todoList, Tasks tasks)
        {
            var todoItems = AppFuncs.GetFlatTodoList(todoList);
            var todoItemMap = BuildTodoMap(todoItems);
            var taskItems = tasks.items;

            _newIdMap = new Dictionary<int, string>();

            var toDeleteTasks = new List<Task>();
            for (var n = taskItems.Count - 1; n >= 0; --n)
            {
                var task = taskItems[n];
                if (!todoItemMap.ContainsKey(task.id))
                {
                    toDeleteTasks.Add(task);
                    taskItems.RemoveAt(n);
                }
            }
            for (var n = toDeleteTasks.Count - 1; n >= 0; --n)
            {
                var task = toDeleteTasks[n];
                TaskManager.Instance.Delete(taskListId, task.id, _token);
            }
            var taskItemMap = BuildTaskMap(taskItems);

            //
            _changed = toDeleteTasks.Count > 0;
            for (var n = 0; n < todoItems.Count; n++)
            {
                //unabortable
//                if(_aborting)
//                {
//                    return;
//                }
                var todo = todoItems[n];
                var todoId = todo.Id;

                var task = n < taskItems.Count ? taskItems[n] : null;
                var taskId = task == null ? null : task.id;

                if (todoId == taskId && task != null && IsSameParent(todo, task))
                {
                    //check content update
                    CheckContentUpdate(todo, task);
                }
                else if (string.IsNullOrEmpty(todoId) || !taskItemMap.ContainsKey(todoId))
                {
                    if (!string.IsNullOrEmpty(todoId))
                    {
                        Debug.Print("Warning: restoring server deleted task? " + todo.Title);
                    }
                    //insert
                    var parentId = todo.Parent != null ? todo.Parent.Id : null;
                    var prevId = GetPrevTodoId(todoItems, n);
                    task = new Task {
                        title = todo.Title,
                        notes = PackTaskNotes(todo),
                        due = ParseTodoDue(todo.Due),
                        status = todo.Checked == 1 ? "completed" : "needsAction",
                        completed = todo.CheckTime
                    };
                    task = TaskManager.Instance.Insert(taskListId, task, parentId, prevId, _token);
                    
                    todo.Id = task.id;
                    _newIdMap[n] = task.id;

                    taskItems.Insert(n, task);
                    taskItemMap[task.id] = task;
                    _changed = true;
                }
                else
                {
                    Debug.Assert(task != null);

                    var parentId = todo.Parent != null ? todo.Parent.Id : null;
                    var prevId = GetPrevTodoId(todoItems, n);
                    TaskManager.Instance.Move(taskListId, todoId, parentId, prevId, _token);

                    var movingTask = taskItemMap[todoId];
                    // update parent
                    movingTask.parent = parentId;

                    //taskItems.Remove(matchingTask);
                    //taskItems.Insert(n, matchingTask);
                    MoveTaskItemTo(taskItems, movingTask, n);
                    _changed = true;

                    //
                    CheckContentUpdate(todo, movingTask);
                }
            }
            // truncate left tasks...
//            for (var n = todoItems.Count; n < taskItems.Count; n++)
//            {
//                var task = taskItems[n];
//                TaskManager.Instance.Delete(taskListId, task.id, _token);
//            }
//
            //
            if (_changed)
            {
                var updateTime = TaskListManager.Instance.GetUpdateTime(taskListId, _token);
                if (Math.Abs((updateTime - todoList.UpdateTime).TotalSeconds) >= 1)
                {
                    Console.WriteLine("write update time");
                    todoList.UpdateTime = updateTime;
                    TodoListWriter.Save(todoList, _filePath);
                }
            }
            Console.WriteLine("synchronized.. @" + DateTime.Now);
        }

        private string PackTaskNotes(Todo todo)
        {
            var sb = new StringBuilder();
            sb.Append('{');
            if (todo.Active == 1)
            {
                sb.Append('A');
            }
            if (todo.Priority != 0)
            {
                sb.Append('P').Append(todo.Priority);
            }
            if (todo.Tags.Count > 0)
            {
                sb.Append("T(").Append(string.Join(",", todo.Tags.ToArray())).Append(')');
            }
            if (!string.IsNullOrEmpty(todo.Color))
            {
                sb.Append("C(").Append(todo.Color).Append(')');
            }
            if (todo.DisplayStyle.HasFlag(DisplayStyle.Bold))
            {
                sb.Append('B');
            }
            if (todo.DisplayStyle.HasFlag(DisplayStyle.Underline))
            {
                sb.Append('U');
            }
            if (todo.DisplayStyle.HasFlag(DisplayStyle.Italic))
            {
                sb.Append('I');
            }
            if (todo.Progress != 0)
            {
                sb.Append("G(").Append(todo.Progress).Append(')');
            }
            if (sb.Length > 1)
            {
                sb.Append('}');
            }
            else
            {
                sb.Length = 0;
            }
            sb.Append(todo.Description);
            return sb.ToString();
        }

        private string GetPrevTodoId(List<Todo> todos, int index)
        {
            var todo = todos[index];
            for (var n = index - 1; n >= 0; --n)
            {
                var prevTodo = todos[n];
                if (prevTodo.Parent == todo.Parent)
                {
                    return prevTodo.Id;
                }
            }
            return null;
        }

        private bool IsSameParent(Todo todo, Task task)
        {
            if (string.IsNullOrEmpty(task.parent))
            {
                return todo.Parent == null;
            }
            if (todo.Parent == null)
            {
                return false;
            }
            return todo.Parent.Id == task.parent;
        }

        private void MoveTaskItemTo(List<Task> tasks, Task task, int toIndex)
        {
            var index = tasks.IndexOf(task);
            Debug.Assert(index > toIndex);
            var index2 = index;
            var movingItems = new List<Task>();
            movingItems.Add(task);
            for (var n = index + 1; n < tasks.Count; n++)
            {
                var tTask = tasks[n];
                if (tTask.parent == task.parent)
                {
                    break;
                }
                index2 = n;
                movingItems.Add(tTask);
            }
            tasks.RemoveRange(index, index2 - index + 1);
            tasks.InsertRange(toIndex, movingItems);
        }

        private DateTime ParseTodoDue(string due)
        {
            if (string.IsNullOrEmpty(due))
            {
                return DateTime.MinValue;
            }
            try
            {
                return DateTime.ParseExact(due, "yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture);
            }
            catch
            {
                return DateTime.MinValue;
            }
        }

        private void CheckContentUpdate(Todo todo, Task task)
        {
            var taskId = task.id;

            var diff = new Dictionary<string, object>();
            if (todo.Title != task.title)
            {
                diff["title"] = todo.Title;
            }
            var notes = PackTaskNotes(todo);
            if (notes != (task.notes ?? ""))
            {
                diff["notes"] = notes;
            }
            var todoChecked = (todo.Checked == 1);
            var taskCompleted = task.status == "completed";
            var checkedChanged = todoChecked != taskCompleted;
            var checkTimeChanged = Math.Abs((todo.CheckTime - task.completed).TotalMinutes) >= 1;

            if (checkedChanged || checkTimeChanged)
            {
                if (todoChecked)
                {
                    diff["status"] = "completed";
                    diff["completed"] = todo.CheckTime == DateTime.MinValue
                        ? null
                        : (object) todo.CheckTime;
                }
                else
                {
                    diff["status"] = "needsAction";
                    diff["completed"] = null;
                }
            }
            try
            {
                var todoDue = string.IsNullOrEmpty(todo.Due)
                    ? DateTime.MinValue
                    : DateTime.ParseExact(todo.Due, "yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture);
                if (todoDue != task.due)
                {
                    if (todoDue == DateTime.MinValue)
                    {
                        diff["due"] = null;
                    }
                    else
                    {
                        diff["due"] = todoDue;
                    }
                }
            }
            catch (Exception e)
            {
                //ignore
            }
            if (diff.Count == 0)
            {
                return;
            }

            //
            var url = "https://www.googleapis.com/tasks/v1/lists/" + taskListId + "/tasks/" + taskId + "?fields=id";

            AuthManager.Instance.AuthExecute(() => {
                var req = HttpUtils.CreateRequest(url);
                req.Method = "PATCH";
                req.ContentType = "application/json";
                var json = JSON.Instance.ToJSON(diff);

                Debug.Print("@@@@@ [PATCH] taskId: " + taskId + ", changes: " + json);

                using (var sw = new StreamWriter(req.GetRequestStream()))
                {
                    sw.Write(json);
                    sw.Flush();
                }
                return req;
            }, req => {
                var rsp = req.GetResponse() as HttpWebResponse;
                string text;
                using (var reader = new StreamReader(rsp.GetResponseStream()))
                {
                    text = reader.ReadToEnd();
                }
                rsp.Close();
                //Debug.Assert(json.id == todo.Id && text == taskId);
                _changed = true;
            });
        }

        private Dictionary<string, Task> BuildTaskMap(List<Task> tasks)
        {
            var map = new Dictionary<string, Task>();
            foreach (var task in tasks)
            {
                map[task.id] = task;
            }
            return map;
        }

        private Dictionary<string, Todo> BuildTodoMap(List<Todo> todos)
        {
            var map = new Dictionary<string, Todo>();
            foreach (var todo in todos)
            {
                if (!string.IsNullOrEmpty(todo.Id))
                {
                    map[todo.Id] = todo;
                }
            }
            return map;
        }

        public void AbortProcessing()
        {
            _aborting = true;
            AuthManager.Instance.AbortProcessing();
            TaskManager.Instance.AbortProcessing();
        }
    }
}